/**
 * Copyright (c) iwindplus Technologies Co., Ltd.2024-2030, All rights reserved.
 */

package com.iwindplus.boot.operate.aspect;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.http.useragent.Browser;
import cn.hutool.http.useragent.OS;
import cn.hutool.http.useragent.Platform;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.iwindplus.boot.domain.constant.CommonConstant.SymbolConstant;
import com.iwindplus.boot.domain.context.HeaderContextHolder;
import com.iwindplus.boot.domain.context.UserContextHolder;
import com.iwindplus.boot.domain.vo.ResultVO;
import com.iwindplus.boot.domain.vo.TraceInfoVO;
import com.iwindplus.boot.domain.vo.UserBaseVO;
import com.iwindplus.boot.operate.domain.annotation.OperateLog;
import com.iwindplus.boot.operate.domain.dto.OperateLogDTO;
import com.iwindplus.boot.operate.domain.event.OperateLogEvent;
import com.iwindplus.boot.operate.domain.property.OperateLogProperty;
import com.iwindplus.boot.util.AddressUtil;
import com.iwindplus.boot.util.DatesUtil;
import com.iwindplus.boot.util.ExpressionUtil;
import com.iwindplus.boot.util.HttpsUtil;
import com.iwindplus.boot.util.JacksonUtil;
import com.iwindplus.boot.util.domain.vo.AddressVO;
import com.iwindplus.boot.web.manager.context.ContextTransManager;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;

/**
 * 操作日志拦截.
 *
 * @author zengdegui
 * @since 2024/4/11
 */
@Slf4j
@Aspect
public class OperateLogAspect {

    @Resource
    private ContextTransManager contextTransManager;

    @Value("${spring.application.name}")
    private String applicationName;

    @Resource
    private OperateLogProperty property;

    @Resource
    private KeyGenerator keyGenerator;

    @Resource
    private ApplicationContext applicationContext;

    @Pointcut("@within(com.iwindplus.boot.operate.domain.annotation.OperateLog) || @annotation(com.iwindplus.boot.operate.domain.annotation.OperateLog)")
    public void pointCutMethod() {
    }

    @Around("pointCutMethod()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        final MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        OperateLog operateLog = method.getAnnotation(OperateLog.class);
        if (Boolean.FALSE.equals(this.property.getEnabled()) || Boolean.FALSE.equals(operateLog.enabled())) {
            return joinPoint.proceed();
        }
        final long beginMillis = System.currentTimeMillis();
        log.info("操作日志审计，开始时间={}", DatesUtil.parseDate(beginMillis, DatePattern.NORM_DATETIME_MS_PATTERN));

        Object target = joinPoint.getTarget();
        String[] argNames = methodSignature.getParameterNames();
        final Object[] args = joinPoint.getArgs();
        // 执行方法
        Object result = joinPoint.proceed();
        final long endMillis = System.currentTimeMillis();
        try {
            final OperateLogDTO entity = this.buildOperateLog(operateLog, method, argNames, args, beginMillis, target, result, endMillis);
            if (Optional.ofNullable(entity).map(OperateLogDTO::getBizNumber).isPresent()) {
                log.info("操作日志审计发布事件");
                this.applicationContext.publishEvent(new OperateLogEvent(this, entity));
            }
        } catch (Exception ex) {
            log.error("操作日志审计异常", ex);
        } finally {
            log.info("操作日志审计，应用名称={}, 结束时间={}，总执行毫秒数={}", this.applicationName
                , DatesUtil.parseDate(endMillis, DatePattern.NORM_DATETIME_MS_PATTERN), endMillis - beginMillis);
        }

        return result;
    }

    private OperateLogDTO buildOperateLog(OperateLog entity, Method method, String[] argNames, Object[] args,
        long beginMillis, Object target, Object result, long endMillis) {
        boolean flag = this.conditionFlag(entity, method, args);
        if (Boolean.TRUE.equals(flag)) {
            return null;
        }
        final TraceInfoVO traceInfo = this.contextTransManager.getTraceInfo();
        String realIp = this.getRealIp(traceInfo);
        final UserBaseVO userInfo = this.getUserInfo();
        final OperateLogDTO param = OperateLogDTO.builder()
            .targetServer(this.applicationName)
            .bizNumber(this.buildBizNumber(entity, target, method, args))
            .bizType(entity.bizType())
            .operateType(entity.operateType())
            .operateName(entity.operateName())
            .operateDesc(CharSequenceUtil.isNotBlank(entity.operateDesc()) ? entity.operateDesc() : entity.operateName())
            .responseBody(this.buildResponseBody(result))
            .requestBeginTime(DatesUtil.parseDate(beginMillis, DatePattern.NORM_DATETIME_MS_PATTERN))
            .requestEndTime(DatesUtil.parseDate(endMillis, DatePattern.NORM_DATETIME_MS_PATTERN))
            .executeTime(endMillis - beginMillis)
            .traceId(traceInfo.getTraceId())
            .ip(realIp)
            .userId(userInfo.getUserId())
            .orgId(userInfo.getOrgId())
            .createdBy(userInfo.getRealName())
            .createdId(userInfo.getUserId())
            .modifiedBy(userInfo.getRealName())
            .modifiedId(userInfo.getUserId())
            .build();
        final HttpServletRequest httpServletRequest = HttpsUtil.getHttpServletRequest();
        if (ObjectUtil.isNotEmpty(httpServletRequest)
            && CharSequenceUtil.isNotBlank(httpServletRequest.getContentType())
            && httpServletRequest.getContentType().contains(MediaType.APPLICATION_JSON.toString())) {
            String requestBody = this.buildRequestBody(args);
            param.setRequestBody(requestBody);
        } else {
            final String requestParam = this.buildRequestParam(argNames, args);
            param.setQueryParams(requestParam);
        }
        this.buildSystemInfo(param);
        this.buildLocation(param);
        return param;
    }

    private String getRealIp(TraceInfoVO traceInfo) {
        String realIp = traceInfo.getRealIp();
        if (CharSequenceUtil.isBlank(realIp)) {
            final HttpServletRequest httpServletRequest = HttpsUtil.getHttpServletRequest();
            if (Objects.nonNull(httpServletRequest)) {
                realIp = JakartaServletUtil.getClientIP(httpServletRequest);
            }
        }
        return realIp;
    }

    private boolean conditionFlag(OperateLog entity, Method method, Object[] args) {
        boolean conditionFlag = false;
        String[] conditions = entity.conditions();
        if (ArrayUtil.isNotEmpty(conditions) && ArrayUtil.isNotEmpty(args)) {
            final List<Boolean> resultList = ExpressionUtil.parse(method, args, conditions, Boolean.class);
            if (CollUtil.isNotEmpty(resultList) && resultList.stream().allMatch(Boolean.TRUE::equals)) {
                conditionFlag = true;
            }
        }
        return conditionFlag;
    }

    private String buildBizNumber(OperateLog entity, Object target, Method method, Object[] args) {
        String[] keys = entity.keys();
        if (ArrayUtil.isNotEmpty(keys)) {
            final List<Object> resultList = ExpressionUtil.parse(method, args, keys, Object.class);
            if (CollUtil.isNotEmpty(resultList)) {
                return resultList.stream().map(Object::toString).collect(Collectors.joining(SymbolConstant.UNDERLINE));
            }
        }
        return this.keyGenerator.generate(target, method, args).toString();
    }

    private Map<String, Object> buildArgsMap(String[] argNames, Object[] args) {
        Map<String, Object> argsMap = new HashMap<>(16);
        if (ArrayUtil.isNotEmpty(argNames) && ArrayUtil.isNotEmpty(args)) {
            for (int i = 0; i < argNames.length; i++) {
                argsMap.put(argNames[i], args[i]);
            }
        }
        return argsMap;
    }

    private String buildRequestBody(Object[] args) {
        if (Boolean.FALSE.equals(this.property.getEnabledRequestBody())) {
            return null;
        }
        if (ArrayUtil.isEmpty(args)) {
            return null;
        }
        return JacksonUtil.toJsonStr(args);
    }

    private String buildRequestParam(String[] argNames, Object[] args) {
        if (Boolean.FALSE.equals(this.property.getEnabledRequestParam())) {
            return null;
        }
        Map<String, Object> argsMap = this.buildArgsMap(argNames, args);
        if (MapUtil.isEmpty(argsMap)) {
            return null;
        }
        return URLUtil.buildQuery(argsMap, Charset.defaultCharset());
    }

    private String buildResponseBody(Object result) {
        if (Boolean.FALSE.equals(this.property.getEnabledResponseBody())) {
            return null;
        }
        if (Objects.isNull(result)) {
            return null;
        }
        if (Boolean.FALSE.equals(result instanceof ResultVO<?>)) {
            return result.toString();
        }

        return JacksonUtil.toJsonStr(result);
    }

    private void buildSystemInfo(OperateLogDTO entity) {
        String userAgentStr = Optional.ofNullable(HeaderContextHolder.getContext())
            .map(m -> m.get(HttpHeaders.USER_AGENT)).orElse(null);
        if (CharSequenceUtil.isBlank(userAgentStr)) {
            return;
        }
        UserAgent userAgent = UserAgentUtil.parse(userAgentStr);
        entity.setPlatformName(Optional.ofNullable(userAgent).map(UserAgent::getPlatform).map(Platform::getName).orElse(null));
        entity.setOsName(Optional.ofNullable(userAgent).map(UserAgent::getOs).map(OS::getName).orElse(null));
        entity.setBrowserName(Optional.ofNullable(userAgent).map(UserAgent::getBrowser).map(Browser::getName).orElse(null));
    }

    private void buildLocation(OperateLogDTO entity) {
        if (CharSequenceUtil.isBlank(entity.getIp())) {
            return;
        }
        AddressVO result = AddressUtil.getAddress(entity.getIp());
        entity.setProvince(Optional.ofNullable(result).map(AddressVO::getProvince).orElse(null));
        entity.setCity(Optional.ofNullable(result).map(AddressVO::getCity).orElse(null));
    }

    private UserBaseVO getUserInfo() {
        return Optional.ofNullable(UserContextHolder.getContext()).orElse(UserContextHolder.getDefaultUser());
    }
}
